package com.vickw.websocketdemo.util;

/**
 * @version 1.0
 * @Description：
 * @Author VickW
 * @Date 2025/7/19 10:32
 */

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
import java.util.stream.Collectors;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author wanqing
 * @version RsaUtils.java, v 0.1 2025年07月18日 15:51 WQ
 */
public class RsaUtils {

    private static final Logger log = LoggerFactory.getLogger(RsaUtils.class);

    private static final ObjectMapper objectMapper = new ObjectMapper();


    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    private static final String ALGORITHM = "RSA";
    private static final String PROVIDER = "BC";
    private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";

    /**
     * Loads a public key from a PEM formatted string
     *
     * @param pemPublicKey PEM formatted public key string
     * @return PublicKey object
     * @throws Exception if an error occurs during parsing or key generation
     */
    public static PublicKey loadPublicKey(String pemPublicKey) throws Exception {
        try {
            StringBuilder encoded = new StringBuilder();
            BufferedReader reader = new BufferedReader(new StringReader(pemPublicKey));
            String line;
            while ((line = reader.readLine()) != null) {
                if (line.startsWith("-----")) {
                    continue;
                }
                encoded.append(line.trim());
            }
            byte[] pkcs8Bytes = Base64.getDecoder().decode(encoded.toString());
            X509EncodedKeySpec keySpec = new X509EncodedKeySpec(pkcs8Bytes);
            return KeyFactory.getInstance(ALGORITHM, PROVIDER).generatePublic(keySpec);
        } catch (GeneralSecurityException | IOException e) {
            System.out.println((String.format("RsaUtils.loadPublicKey fail, %s", e.getMessage())));
            throw e;
        }
    }


    /**
     * Loads a private key from a PEM formatted string (PKCS#8)
     *
     * @param pemPrivateKey PEM formatted private key string
     * @return PrivateKey object
     * @throws Exception if an error occurs during parsing or key generation
     */
    public static PrivateKey loadPrivateKey(String pemPrivateKey) throws Exception {
        if (pemPrivateKey == null || pemPrivateKey.trim().isEmpty()) {
            System.out.println("pemPrivateKey is null.");
        }
        StringBuilder encoded = new StringBuilder();
        if (pemPrivateKey.startsWith("-----BEGIN")) {
            try {
                extractBase64FromPem(pemPrivateKey, encoded);
            } catch (IOException e) {
                System.out.println("extractBase64FromPem error");
            }
        } else {
            encoded.append(pemPrivateKey.trim());
        }

        if (encoded.isEmpty()) {
            System.out.println("encoded is empty.");
        }
        try {
            byte[] pkcs8Bytes = Base64.getDecoder().decode(encoded.toString());
            PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(pkcs8Bytes);
            return KeyFactory.getInstance(ALGORITHM, PROVIDER).generatePrivate(keySpec);
        } catch (GeneralSecurityException e) {
            System.out.println(String.format("RsaUtils.loadPrivateKey fail, %s", e.getMessage()));
            throw e;
        }
    }

    /**
     * Extracts the Base64 content from a PEM formatted private key string.
     */
    private static void extractBase64FromPem(String pemContent, StringBuilder encoded) throws IOException {
        boolean isContentSection = false;
        try (BufferedReader reader = new BufferedReader(new StringReader(pemContent))) {
            String line;
            while ((line = reader.readLine()) != null) {
                String trimmedLine = line.trim();
                if (trimmedLine.startsWith("-----BEGIN") && trimmedLine.contains("PRIVATE KEY")) {
                    isContentSection = true;
                } else if (trimmedLine.startsWith("-----END") && trimmedLine.contains("PRIVATE KEY")) {
                    break;
                } else if (isContentSection && !trimmedLine.isEmpty()) {
                    encoded.append(trimmedLine);
                }
            }
        }
    }


    /**
     * Signs the given data using the provided private key
     *
     * @param privateKey The private key used for signing
     * @return Base64-encoded signature
     * @throws Exception if an error occurs during signing
     */
    public static String sign(Object head, Object body, PrivateKey privateKey) throws Exception {
        try {
            Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM, PROVIDER);
            signature.initSign(privateKey);
            signature.update(buildJsonString(head, body).getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(signature.sign());
        } catch (GeneralSecurityException e) {
            System.out.println(String.format("RsaUtils.sign fail, %s", e.getMessage()));
            throw e;
        }
    }

    /**
     * Verifies a digital signature using the provided public key
     *
     * @param publicKey Public key used for verification
     * @param signatureStr Base64-encoded signature string
     * @return true if the signature is valid, false otherwise
     */
    public static boolean verify(Object head, Object body, PublicKey publicKey, String signatureStr) throws Exception {
        try {
            Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM, PROVIDER);
            signature.initVerify(publicKey);
            signature.update(buildJsonString(head, body).getBytes(StandardCharsets.UTF_8));
            return signature.verify(Base64.getDecoder().decode(signatureStr));
        } catch (Exception e) {
            System.out.println(String.format("RsaUtils.verify fail, %s", e.getMessage()));
            throw e;
        }
    }

    public static Map<String, Object> filterAndSort(Object obj) {
        TreeMap<String, Object> resultMap = new TreeMap<>();
        try {
            // Convert object to JsonNode
            JsonNode jsonNode = objectMapper.valueToTree(obj);

            // Iterate over all fields
            Iterator<Map.Entry<String, JsonNode>> fields = jsonNode.fields();
            while (fields.hasNext()) {
                Map.Entry<String, JsonNode> entry = fields.next();
                JsonNode valueNode = entry.getValue();

                // Filter empty values
                if (shouldFilter(valueNode)) {
                    continue;
                }

                // Process nested objects (convert to JSON string)
                if (isNestedObject(valueNode)) {
                    resultMap.put(entry.getKey(), valueNode.toString());
                } else {
                    // Store basic types directly
                    resultMap.put(entry.getKey(), getBasicValue(valueNode));
                }
            }
        } catch (Exception e) {
            System.out.println(String.format("RsaUtils.verify fail, %s", e.getMessage()));
            throw e;
        }
        return resultMap;
    }

    private static boolean shouldFilter(JsonNode node) {
        if (node.isNull()) {
            return true;
        }

        // Check empty string
        if (node.isTextual() && node.asText().isEmpty()) {
            return true;
        }

        // Check empty array/collection
        if (node.isArray() && node.isEmpty()) {
            return true;
        }

        // Check empty object
        return node.isObject() && node.isEmpty();
    }

    private static boolean isNestedObject(JsonNode node) {
        return node.isObject()
                || node.isArray()
                || node.isPojo()
                || node.getNodeType() == JsonNodeType.POJO;
    }

    private static Object getBasicValue(JsonNode node) {
        if (node.isBoolean()) {
            return node.asBoolean();
        } else if (node.isInt()) {
            return node.asInt();
        } else if (node.isLong()) {
            return node.asLong();
        } else if (node.isDouble()) {
            return node.asDouble();
        } else if (node.isTextual()) {
            return node.asText();
        }
        // Convert other unrecognized types to string
        return node.toString();
    }

    /**
     * Builds a JSON string from the given SignatureHead and body object.
     *
     * @param head SignatureHead containing metadata for the request
     * @param body Object containing the request body data
     * @return JSON string representation of the request
     */
    public static String buildJsonString(Object head, Object body) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("head", filterAndSort(head));
        map.put("body", filterAndSort(body));
        return JSON.toJSONString(map);
    }


    public static void main(String[] args) {
        JSONObject signatureHeadBody = new JSONObject();
        signatureHeadBody.put("app", "chatbot");
        signatureHeadBody.put("reqTime", System.currentTimeMillis());
        signatureHeadBody.put("reqMsgId", UUID.randomUUID());

        JSONObject requestBody = new JSONObject();
        requestBody.put("signatureHead", signatureHeadBody);


        Map<String, Object> result = filterAndSort(requestBody);
        //Map<String, Object> result = new LinkedHashMap<>();
        //
        //List<Map.Entry<String, Object>> toSort = new ArrayList<>();
        //for (Map.Entry<String, Object> e : requestBody.entrySet()) {
        //    if (e.getValue() != null && !(e.getValue() instanceof Map && ((Map<?, ?>) e.getValue()).isEmpty())) {
        //        toSort.add(e);
        //    }
        //}
        //toSort.sort((e1, e2) -> e1.getKey().toString().compareTo(e2.getKey().toString()));
        //for (Map.Entry<String, Object> e : toSort) {
        //    result.put(e.getKey().toString(), filterAndSort(e.getValue()));
        //}

        System.out.println(JSON.toJSONString(result));


    }

    /**
     * sort and filter the map.
     *
     * @param obj input obj
     * @return sorted and filtered map
     */
    //public static Map<String, Object> filterAndSortMap(Object obj) {
    //    if (obj == null) {
    //        return null;
    //    }
    //    if (obj instanceof Map<?, ?> original) {
    //        //Map<?, ?> original = (Map<?, ?>) obj;
    //        Map<String, Object> result = new LinkedHashMap<>();
    //        original.entrySet().stream()
    //                .filter(e -> e.getValue() != null && !(e.getValue() instanceof Map && ((Map<?, ?>) e.getValue()).isEmpty()))
    //                .sorted((e1, e2) -> e1.getKey().toString().compareTo(e2.getKey().toString()))
    //                .forEach(e -> result.put(e.getKey().toString(), filterAndSortMap(e.getValue())));
    //        return result;
    //    }
    //    // 处理普通JavaBean
    //    if (obj.getClass().getPackage() != null && obj.getClass().getPackage().getName().startsWith("java")) {
    //        Map<String, Object> result = new LinkedHashMap<>();
    //        result.put(""+obj, obj);
    //        return result;
    //    }
    //    Map<String, Object> result = new LinkedHashMap<>();
    //    Arrays.stream(obj.getClass().getDeclaredFields()).sorted(Comparator.comparing(f -> f.getName())).forEach(field -> {
    //        field.setAccessible(true);
    //        try {
    //            Object value = field.get(obj);
    //            if (value != null && !(value instanceof Map && ((Map<?, ?>) value).isEmpty())) {
    //                result.put(field.getName(), filterAndSortMap(value));
    //            }
    //        } catch (IllegalAccessException ignored) {
    //        }
    //    });
    //    return result;
    //}
}

